/**********************************************************************
 *   Copyright: (C)2024 LingYun IoT System Studio
 *      Author: LiJiahui<2199250859@qq.com>
 *
 * Description: Temperature sensor DS18B20 driver on ISKBoard
 *
 *   ChangeLog:
 *        Version    Date       Author            Description
 *        V1.0.0  2024.08.29    LiJiahui      Release initial version
 *
 ***********************************************************************/

#include "main.h"
#include "miscdev.h" /* 此处需要使用微秒级延时函数 udelay() */

/* 使用 HAL_Delay() 定义一个新的毫秒级的延时宏 mdelay()， 与Linux内核风格保持一致 */
#define mdelay(ms) HAL_Delay(ms)

/* 定义一个GPIO口的结构体类型，有两个成员 */
typedef struct w1_gpio_s
{
	GPIO_TypeDef 	*group; /* 该GPIO口在哪个组 */
	uint16_t		pin;	/* 该GPIO口的引脚号 */
} w1_gpio_t;

/* DS18B20 上的数据通信口 DQ 连到了 CPU 的 PA12 引脚上, 结构体变量初始化 */
static w1_gpio_t	W1Dat =
{
		.group = GPIOA,
		.pin = GPIO_PIN_12,
};

/*--------------------------------------------------------------------------------------------
 *  此处定义了一些DS18B20通信DQ引脚的操作宏，
 * 此处使用宏而非函数原因： 1：减少函数调用开销，宏是预处理阶段直接替换，没有函数调用的堆栈操作，
 * 适合对性能高的场景（如GPIO操作）；
 * 2：避免函数调用的额外指令：函数调用需要保存/恢复寄存器、传递参数等，而宏直接嵌入代码，效率更高
 * 3：适用于简单操作：像GPIO读写这样的简单操作， 用宏更合适
 * 出现断行符原因：C/C++宏定义默认只能写在一行，但复杂的宏可能很长，直接写在一行会导致：
 * 代码难以阅读、难以维护、容易出错，‘\‘的作用就是告诉编译器这一行还没结束，下一行继续
 * -------------------------------------------------------------------------------------------
 * */

/* 此处定义了一个宏， 用来初始化DQ引脚：设置为输入模式，并使能内部上拉（输出是OC门，必须接上拉电阻，目的是提高电平的稳定性） */
#define W1DQ_Input()	\
{	\
	GPIO_InitTypeDef GPIO_InitStruct = {0};	\
	GPIO_InitStruct.Pin = W1Dat.pin;	\
	GPIO_InitStruct.Mode = GPIO_MODE_INPUT;	\
	GPIO_InitStruct.Pull = GPIO_PULLUP;	\
	GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;	\
	HAL_GPIO_Init(W1Dat.group, &GPIO_InitStruct);	\
}

/* 此处定义了一个宏， 用来把DQ引脚设置为推挽输出模式 ,此处应设置为开漏输出模式，但由于严格控制时序，不会存在电平冲突的问题，也可改为GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD*/
#define W1DQ_Output()	\
{	\
	GPIO_InitTypeDef GPIO_InitStruct = {0};	\
	GPIO_InitStruct.Pin = W1Dat.pin;	\
	GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;	\
	GPIO_InitStruct.Pull = GPIO_NOPULL;	\
	GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;	\
	HAL_GPIO_Init(W1Dat.group, &GPIO_InitStruct);	\
}

/* 此处定义了一个宏， 用来写控制DQ引脚输出高电平还是低电平 */
#define W1DQ_Write(x)	HAL_GPIO_WritePin(W1Dat.group, W1Dat.pin,\
		(x == 1)? GPIO_PIN_SET:GPIO_PIN_RESET)

/* 此处定义了一个宏，用来读DQ引脚，当前是高电平还是低电平 */
#define W1DQ_Read()		HAL_GPIO_ReadPin(W1Dat.group, W1Dat.pin)

/* 此处定义了一个枚举， 用来定义低电平和高电平 */
enum
{
	LOW, /* 低电平：LOW = 0 */
	HIGH, /* 高电平，：HIGH = 1 */
};

/* 单总线上的所有通信都是以初始化序列开始，Master发出初始化信号后等待从设备的应答信号， 以确定从设备是否存在并能正常工作 */
static int ds18b20_reset(void)
{
	int	time = 0;
	int	present = 0; /* 设置默认值没有探测到DS18B20 */

	/* 设置DQ引脚为输出模式 */
	W1DQ_Output();

	/* 主机（CPU）输出低电平， 并保持低电平时间至少480us， 以产生复位脉冲 */
	W1DQ_Write(LOW);
	udelay(480);

	/* 接着主机（CPU）释放总线， 4，7K的上拉电阻将单总线拉高， 延时15~60us， 并进入接收模式（Rx） */
	W1DQ_Write(HIGH);
	udelay(60);
	W1DQ_Input();

	/* DS18B20芯片在收到主机发送过来的这个复位脉冲后，将会拉低总线60~240us， 以产生低电平应答脉冲
	 * W1DQ_Read()代表读到高电平
	 * */
	while( W1DQ_Read() && time<240 )
	{
		time++;
		udelay(1);
	}

	/* 如果time<240 则说明上面的while()循环是因为W1DQ_Read()读到了低电平跳出的，而不是超时跳出
	 * 这也就意味着CPU在这段时间读到了低电平，探测到了DS18B20芯片
	 *  */
	if(time < 240)
	{
		present = 1;
	}

	/* 释放总线并维持至少480us */
	W1DQ_Output();
	udelay(480 - time);

	return present;
}

/*
 * 写 0 时序：主机输出低电平， 延时60us， 然后释放总线， 延时2us；
 * 写 1 时许：主机输出高电平， 延时2us， 然后释放总线，延时60us；
 */
void ds18b20_write_byte(uint8_t byte)
{
	uint8_t	i = 0;

	/* 设置DS18B20的DQ引脚为输出模式 */
	W1DQ_Output();

	/* 1个字节包含8个为， DS18B20数据发送是LSB（低位先发送） */
	for(i=0; i<8; i++)
	{
		/* DS18B20的写0逻辑是将DQ拉低>=60us后拉高，写1逻辑是拉低<=15us后拉高 */

		/* 这里先将DS18B20 的DQ先拉低 10us(<15us), 为了兼容写1/0电平，后面可切换 */
		W1DQ_Write(LOW);
		udelay(10);

		/* 判断当前位是1还是0 ,byte & 0x1提取最低位 */
		if(byte & 0x1)
		{
			W1DQ_Write(HIGH); /* 如果当前位是1，则现在就拉成高电平，这样低电平维持时间10us */
		}
		else
		{
			W1DQ_Write(LOW); /* 如果当前位是0，则急需维持低电平，这样低电平维持时间为：10us+下面的60us */
		}

		/* 无论是写0还是写1，DQ的写时序都要大于15+15+30 = 60us */
		udelay(60);

		/* 发送0/1数据位完成后，拉高总线并延时2us */
		W1DQ_Write(HIGH);
		udelay(2);

		/* 该字节数据右移一位，即发下一个高位数据 */
		byte >>= 1;
	}
}

/* 当总线控制器把数据线从高电平拉到低电平时，读时序开始，数据线必须至少保持1us，然后总线被释放
 * DS18B20通过拉高或拉低总线来传输“1”或“0”
 */
uint8_t ds18b20_read_byte(void)
{
	uint8_t	i = 0;
	uint8_t	byte = 0;

	for(i=0; i<8; i++)
	{
		/* 当CPU把数据线DQ拉低至少1us后，读时序开始 */
		W1DQ_Output();
		W1DQ_Write(LOW);
		udelay(2);

		/* CPU拉高DQ释放总线 */
		W1DQ_Write(HIGH);
		udelay(2);

		/* CPU设置DQ为输入模式，等待DS18B20发数据过来，注意此时需要靠上拉电阻将总线拉成高电平 */
		W1DQ_Input();

		/* DS18B20开始以LSB模式发送数据 */
		if(W1DQ_Read())
		{
			byte |= 1<<i; /* 如果接收到该位时高电平，则将相应位设为1，否则为0 (byte = ( byte | (1<<i) ))*/
		}

		/* 读时序至少维持15+45=60us */
		udelay(60);

		/* CPU拉高DQ并释放总线 */
		W1DQ_Output();
		udelay(2);
	}

	return byte;
}

/* 该函数用来给DS18B20传感器发送开始采样温度命令,
 * static inline类型说明：static:限制函数作用域为当前文件，避免命名冲突
 * inline：建议编译器内联展开（直接插入调用处），减小函数调用开销（适合频繁调用的小函数）
 * */
static inline int ds18b20_convert(void)
{
	/* 发送复位信号，看DS18B20模块是否在位，如果模块不在或坏了就返回错误值（-1） */
	if( !ds18b20_reset() )
		return -1;

	/* ROM操作(识别和选择总线上的特定传感器)，这里不关心ROM中的16位产品序列号， 就发送skip ROM命令跳过SN的读取 */
	ds18b20_write_byte(0xCC);

	/* 发送Convert T命令，通知DS18B20开始温度采样（0X44） */
	ds18b20_write_byte(0x44);

	return 0;
}

/* 根据DS18B20的CRC校验公式（CRC = X^8 + X^5 + X^4 + 1）(该公式可通过查阅DS18B20手册得到)来检测收到的数据是否出错 */
static int ds18b20_checkcrc(uint8_t *data, uint8_t length, uint8_t checksum)
{
	uint8_t		i, j, byte;
	uint8_t		mix = 0;
	uint8_t 	crc = 0;

	for(i=0; i<length; i++)
	{
		byte = data[i];

		for(j=0; j<8; j++)
		{
			mix = (crc ^ byte) & 0x01;
			crc >>= 1;
			if(mix)
				crc ^= 0x8C; /* 1000 1100 */
			byte >>= 1;
		}
	}

	return crc == checksum ? 1:0;
}

/* 读取DS18B20的采样温度值 */
static inline int ds18b20_read(uint8_t *out, int bytes)
{
	uint8_t	buf[9];
	uint8_t	i = 0;

	/* CPU给DS18B20 发送复位信号并探测芯片是否存在 */
	if( !ds18b20_reset() )
		return -1;

	/* ROM操作(识别和选择总线上的特定传感器)，这里不关心ROM中的16位产品序列号， 就发送skip ROM命令跳过SN的读取 */
	ds18b20_write_byte(0xCC);

	/* 发送Read Scratchpad 命令开始读取DS18B20的采样温度值（0xBE） */
	ds18b20_write_byte(0xBE);

	buf[i++] = ds18b20_read_byte(); /* 第一个字节是温度值的低字节（LSB） */
	buf[i++] = ds18b20_read_byte(); /* 第二个字节是温度值的高字节（MSB） */
	buf[i++] = ds18b20_read_byte(); /* 第三个字节不关心 */
	buf[i++] = ds18b20_read_byte(); /* 第四个字节不关心 */
	buf[i++] = ds18b20_read_byte(); /* 第五个字节是用户配置寄存器，也不关心 */
	buf[i++] = ds18b20_read_byte(); /* 第六个字节保留（OXFF） */
	buf[i++] = ds18b20_read_byte(); /* 第七个字节保留 */
	buf[i++] = ds18b20_read_byte(); /* 第八个字节保留 */
	buf[i++] = ds18b20_read_byte(); /* 第九个字节是CRC校验和 */

	/* 将读取到的前八个字节数据按照CRC公式计算校验和，与第九个字节做对比看是否相等 */
	if( !ds18b20_checkcrc(buf, 8, buf[8]) )
		return -2; /* CRC校验失败就返回错误值 */

	/* CRC通过就输出独到的2字节温度采样原始数据 */
	out[0] = buf[0];
	out[1] = buf[1];

	return 0;
}

int ds18b20_sample(float *temperature)
{
	uint8_t			byte[2];
	uint8_t			sign;
	uint8_t			temp;
	static uint8_t	firstin = 1; /* 是否为第一次采样（ 1：是； 0：否 ） */

	if( !temperature )
		return -1;

	/* 发送开始采样的命令 */
	if( ds18b20_convert() < 0 )
		return -2;

	/* DS18B20 datasheet 说明第一次启动采样最大需要750ms完成，否则将会在100ms内完成采样 */
	if( firstin )
	{
		mdelay(750);
		firstin = 0;
	}
	else
	{
		mdelay(100);
	}

	/* 开始从DS18B20传感器读取2字节的原始数据 */
	if( ds18b20_read(byte, 2) < 0 )
		return -3;

	/* 采样温度值两个字节，共16位。其中byte[0]为低字节、byte[1]为高字节；
	 * LSB：Bits[0:3]为小数位 2^(-4) ~ 2^(-1) Bits[4:7]为整数位:2^0~2^3
	 * MSB: Bits[0:2]为为整数位:2^4~2^6   Bits[3:7]为符号位: 全1表示温度为负值
	*/

	/* 高字节的低3位为温度整数值，高五位为符号位 */
	if(byte[1] > 0x7) /* 如果>7(第三位最大为111=7)，说明为负值 */
	{
		temp = ~(byte[1]<<8 | byte[0]) + 1; /* 此处是补码转原码的步骤，负数的补码是其绝对值的原码取反后＋1 */
		sign = 0; /* 标记为负值 */
	}
	else
	{
		temp = byte[1]<<8 | byte[0];
		sign = 1;
	}

	/* DS18B20默认工作精度为12位，高八位是整数部分，低四位为小数部分。小数部分刻度为0.0625（1/16）
	 * ( temp >>4 )提取整数部分，丢弃小数部分 ； ( temp & 0xF )提取低四位小数 *0.0625转换为实际温度值
	 * */
	*temperature = ( temp >>4 ) + ( temp & 0xF )*0.0625;
	if(!sign)
		*temperature = -*temperature;

	return 0;
}